//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Data;
using EffiProz.Core.Lib;
using EffiProz.Core.DataTypes;



namespace EffiProz.Core
{



    // fredt@users 20020912 - patch 1.7.1 - shortcut treatment of identity() call
    // fredt@users 20020912 - patch 1.7.1 - cache java.lang.reflect.Method objects
    // fredt@users 20021013 - patch 1.7.1 - ignore non-static methods
    // boucherb@users 20030201 - patch 1.7.2 - direct calls for org.hsqldb.Library
    // fredt@users 20030621 - patch 1.7.2 - shortcut treatment of session calls
    // boucherb@users 200404xx - doc 1.7.2 - updates toward 1.7.2 final

    /**
     * Provides services to evaluate SQL function and stored procedure calls,
     * by invoking Java methods.
     *
     * Extended _in successive versions of HSQLDB.
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public class Function
    {

        private string sFunction;
        private MethodInfo mMethod;
        private string returnClassName;
        private Type[] aArgClasses;
        private int iReturnType;
        private int iArgCount;
        private int iSqlArgCount;
        private int iSqlArgStart;
        private int[] iArgType;
        private bool[] bArgNullable;
        public Expression[] eArg;
        private bool bConnection;
        private static Dictionary<string, MethodInfo> methodCache = new Dictionary<string, MethodInfo>();
        private int fID;
        public string name;        // name used to call function
        public bool isSimple;    //CURRENT_TIME, NOW etc.
        public bool hasAggregate;
        private Assembly assembly;

        /**
         * Constructs a new Function object with the given function call name
         * and using the specified Session context. <p>
         *
         * The call name is the fully qualified name of a static Java method, in
         * the form "package.class.method."  This implies that Java
         * methods with the same fully qualified name but different signatures
         * cannot be used properly as HSQLDB SQL functions or stored procedures.
         * For instance, it is impossible to call both System.getProperty(String)
         * and System.getProperty(String,String) under this arrangement, because
         * the HSQLDB Function object is unable to differentiate between the two;
         * it simply chooses the first method matching the FQN _in the array of
         * methods obtained from calling getMethods() on an instance of the
         * Class indicated _in the FQN, hiding all other methods with the same
         * FQN. <p>
         *
         * The function FQN must match at least one static Java method FQN _in the
         * specified class or construction cannot procede and an HsqlException is
         * thrown. <p>
         *
         * The isSimple parameter is true when certain SQL standard functions
         * that are used without brackets are invokded.
         *
         * @param name this Function object's call name
         * @param fqn the fully qualified name of a Java method
         * @param isSimple if true, used to evalate CURRENT_TIME, NOW etc.
         *      evaluate
         * @  if the specified function FQN corresponds to no
         *      Java method
         */
        public Function(string name, string fqn, bool isSimple)
        {

            this.name = name;
            this.isSimple = isSimple;

            //        cSession      = session;
            sFunction = fqn;
            fID = Library.functionID(fqn);

            int i = fqn.LastIndexOf('.');

            Trace.check(i != -1, Trace.UNEXPECTED_TOKEN, fqn);

            string classname = fqn.Substring(0, i - (0));

            mMethod = null;


            if (!methodCache.TryGetValue(fqn, out mMethod))
            {
                string methodname = fqn.Substring(i + 1);
                Type classinstance = null;

                int assemblyMarker = classname.IndexOf("#");
                if (assemblyMarker != -1)
                {//TODOx
                    string assemblyFile = classname.Substring(0, assemblyMarker);
                    classname = classname.Substring(assemblyMarker + 1);
                    
                    try
                    {
                        assembly = Assembly.LoadFrom(assemblyFile);
                    }
                    catch (Exception e)
                    {
                          throw Trace.error(Trace.FUNCTION_NOT_FOUND,
                                      Trace.Message_Pair, new Object[] {
                    classname, e });

                    }
                }

                try
                {
                    if(assembly == null)
                        classinstance = Type.GetType(classname);
                    else
                        classinstance = assembly.GetType(classname);
                }
                catch (Exception e)
                {
                    throw Trace.error(Trace.FUNCTION_NOT_FOUND,
                                      Trace.Message_Pair, new Object[] {
                    classname, e
                });
                }

                // public only, but includes those inherited from
                // superclasses and superinterfaces.  List is unordered.
                MethodInfo[] methods = classinstance.GetMethods();

                for (i = 0; i < methods.Length; i++)
                {
                    MethodInfo m = methods[i];

                    if (m.Name.Equals(methodname, StringComparison.CurrentCultureIgnoreCase)
                            && m.IsStatic)
                    {
                        mMethod = m;

                        break;
                    }
                }

                Trace.check(mMethod != null, Trace.UNKNOWN_FUNCTION, methodname);
                methodCache.Add(fqn, mMethod);
            }

            Type returnClass = mMethod.ReturnType;

            if (returnClass.Equals(typeof(EffiProz.Core.Result)))
            {

                // For now, we can write stored procedures whose
                // descriptor explicitly specifies the above return type.
                // Later, this will be modified or replaced to provide proper
                // support for jdbcCallableStatement OUT mode return parameter,
                // multiple results (Result.MULTI etc.)
                iReturnType = Types.OTHER;
            }
            else
            {

                // Now we support the following construction-time return type
                // Classes, as specified by the method descriptor:
                //
                // 1.) any primitive or primitive wrapper type, except Byte(.TYPE),
                //     Short(.TYPE) and float(.TYPE) (TBD; narrow if no truncation)
                //
                // 2.) any primitive array type
                //
                // 3.) any non-primitive array whose base component implements
                // java.io.Serializable
                //
                // 4.) any class implementing java.io.Serializable, except those
                //     described _in 1.) as currently unsupported
                //
                // 5.) java.lang.Object
                //
                // For java.lang.Object, checking is deferred from the construction
                // stage to the evaluation stage.  In general, for the evaluation
                // to succeed, the runtime class of the retrieved Object must be
                //
                // 1.) any primitive or primitive wrapper type, except Byte(.TYPE),
                //     Short(.TYPE) and float(.TYPE) (TBD; narrow if no trunction)
                //
                // 2.) any primitive array type
                // 3.) any non-primitive array whose base component implements
                //     java.io.Serializable
                //
                // 4.) any class implementing java.io.Serializable, except those
                //     described _in 1.) as currently unsupported
                //
                // Additionally, it is possible for the evaluation to succeed under
                // an SQL CALL if the runtime Class of the returned Object is not
                // from the list above but is from the list below:
                //
                // 1.) is org.hsqldb.Result
                // 2.) is org.hsqldb.jdbc.jdbcResultSet
                //
                // In these special cases, the statement executor notices the
                // types and presents the client with a view the underlying result
                // rather than with a view of the object as an opaque scalar value
                //
                iReturnType = Types.getParameterTypeNr(returnClass);
            }

            returnClassName =
                Types.getFunctionReturnClassName(returnClass.Name);
            ParameterInfo[] _params = mMethod.GetParameters();
            aArgClasses = new Type[_params.Length];
            iArgCount = aArgClasses.Length;
            iArgType = new int[iArgCount];
            bArgNullable = new bool[iArgCount];

            for (i = 0; i < _params.Length; i++)
            {
                ParameterInfo param = _params[i];
                aArgClasses[i] = param.ParameterType;
                Type a = aArgClasses[i];
                string type = a.Name;

                if ((i == 0) && (a.Equals(typeof(IDbConnection))))
                { //TODOx

                    // TODO: provide jdbc:default:connection url functionality
                    //
                    // only the first parameter can be a Connection
                    bConnection = true;
                }
                else
                {

                    // see discussion above for iReturnType
                    iArgType[i] = Types.getParameterTypeNr(a);
                    bArgNullable[i] = !a.IsPrimitive;
                }
            }

            iSqlArgCount = iArgCount;

            if (bConnection)
            {
                iSqlArgCount--;

                iSqlArgStart = 1;
            }
            else
            {
                iSqlArgStart = 0;
            }

            eArg = new Expression[iArgCount];
        }

        /**
         * Evaluates and returns this Function _in the context of the session.<p>
         */
        public Object getValue(Session session)
        {

            switch (fID)
            {

                case Library._curtime:
                    return session.getCurrentTime();

                case Library._curdate:
                    return session.getCurrentDate();

                case Library._database:
                    return session.getDatabase().getPath();

                case Library._getAutoCommit:
                    return session.isAutoCommit() ? true
                                                  : false;

                case Library._identity:
                    return session.getLastIdentity();

                case Library._isReadOnlyDatabase:
                    return session.getDatabase().databaseReadOnly ? true
                                                                  : false;

                case Library._isReadOnlyConnection:
                    return session.isReadOnly() ? true
                                                : false;

                case Library._isReadOnlyDatabaseFiles:
                    return session.getDatabase().isFilesReadOnly() ? true
                                                                   : false;

                case Library._now:
                    return session.getCurrentTimestamp();

                case Library._user:
                    return session.getUsername();
            }

            Object[] oArg = getArguments(session);

            if (oArg == null)
            {
                return null;
            }

            return getValue(session, oArg);
        }

        /**
         * Evaluates the Function with the given arguments _in the session context.
         */
        public Object getValue(Session session,
                          Object[] arguments)
        {

            if (bConnection)
            {
                arguments[0] = session.getInternalConnection();
            }

            try
            {
                Object ret = (fID >= 0) ? Library.invoke(fID, arguments)
                                        : mMethod.Invoke(null, arguments);

                return Column.convertObject(ret, iReturnType);
            }
            catch (TargetException e)
            {

                // thrown by user functions
                Exception t = e.GetBaseException();
                string s = sFunction + " : " + t.ToString();

                throw Trace.error(Trace.FUNCTION_CALL_ERROR, s);
            }
            catch (Exception )
            {

                // never thrown _in this method
                throw Trace.error(Trace.FUNCTION_CALL_ERROR);
            }

            // Library function throw HsqlException
        }

        private Object[] getArguments(Session session)
        {

            int i = bConnection ? 1
                                        : 0;
            Object[] oArg = new Object[iArgCount];

            for (; i < iArgCount; i++)
            {
                Expression e = eArg[i];
                Object o = null;

                if (e != null)
                {

                    // no argument: null
                    o = e.getValue(session, iArgType[i]);
                }

                if ((o == null) && !bArgNullable[i])
                {

                    // null argument for primitive datatype: don't call
                    return null;
                }

                if (o is DotNetObject)
                {
                    o = ((DotNetObject)o).getObject();
                }
                else if (o is Binary)
                {
                    o = ((Binary)o).getBytes();
                }

                oArg[i] = o;
            }

            return oArg;
        }

        /**
         * returns null if any non-nullable element of values is null
         */
        private Object[] getNotNull(Object[] values)
        {

            int i = bConnection ? 1
                                : 0;

            for (; i < iArgCount; i++)
            {
                Object o = values[i];

                if (o == null && !bArgNullable[i])
                {

                    // null argument for primitive datatype: don't call
                    return null;
                }
            }

            return values;
        }

        public void collectInGroupByExpressions(List<Expression> colExps)
        {

            for (int i = 0; i < iArgCount; i++)
            {
                Expression e = eArg[i];

                if (e != null)
                {
                    e.collectInGroupByExpressions(colExps);
                }
            }
        }

        public Object getAggregatedValue(Session session,
                                  Object currValue)
        {

            Object[] valueArray = (Object[])currValue;

            if (valueArray == null)
            {
                valueArray = new Object[iArgCount];
            }

            for (int i = 0; i < iArgCount; i++)
            {
                Expression e = eArg[i];

                if (eArg[i] != null)
                {
                    if (eArg[i].isAggregate())
                    {
                        valueArray[i] = Column.convertObject(
                            e.getAggregatedValue(session, valueArray[i]),
                            iArgType[i]);
                    }
                    else
                    {
                        valueArray[i] = e.getValue(session, iArgType[i]);
                    }
                }
            }

            valueArray = getNotNull(valueArray);

            if (valueArray == null)
            {
                return null;
            }

            return getValue(session, valueArray);
        }

        public Object updateAggregatingValue(Session session,
                                      Object currValue)
        {

            Object[] valueArray = (Object[])currValue;

            if (valueArray == null)
            {
                valueArray = new Object[iArgCount];
            }

            for (int i = 0; i < iArgCount; i++)
            {
                Expression e = eArg[i];

                if (eArg[i] != null)
                {
                    valueArray[i] = e.updateAggregatingValue(session,
                            valueArray[i]);
                }
            }

            return valueArray;
        }

        /**
         * Returns the number of parameters that must be supplied to evaluate
         * this Function object from SQL.  <p>
         *
         * This value may be different than the number of parameters of the
         * underlying Java method.  This is because HSQLDB automatically detects
         * if the first parameter is of type java.sql.Connection, and supplies a
         * live Connection object constructed from the evaluating session context
         * if so.
         */
        public int getArgCount()
        {
            return iSqlArgCount;
        }

        /**
         * Remnoves the Table filters from Expression parameters to this Function.
         *
         * @  if there is a problem resolving a parameter
         * against the specified TableFilter
         */
        /*
            void removeFilters()   {

                Expression e;

                for (int i = iSqlArgStart; i < iArgCount; i++) {
                    e = eArg[i];

                    if (e != null) {
                        e.removeFilters();
                    }
                }
            }
        */
        public void replaceAliases(Expression[] columns,
                            int length)
        {

            Expression e;

            for (int i = iSqlArgStart; i < iArgCount; i++)
            {
                e = eArg[i];

                if (e != null)
                {
                    if (e.exprType == Expression.COLUMN)
                    {
                        eArg[i] = e.getExpressionForAlias(columns, length);
                    }
                    else
                    {
                        e.replaceAliases(columns, length);
                    }
                }
            }
        }

        /**
         * Checks the Expresion parameters to this Function object against the
         * set of TableFilter.
         */
        public void checkTables(List<TableFilter> fa)
        {

            Expression e;

            for (int i = iSqlArgStart; i < iArgCount; i++)
            {
                e = eArg[i];

                if (e != null)
                {
                    e.checkTables(fa);
                }
            }
        }

        /**
         * Resolves the Expression parameters to this Function object against the
         * specified TableFilter.
         */
        public void resolveTables(TableFilter f)
        {

            Expression e;

            for (int i = iSqlArgStart; i < iArgCount; i++)
            {
                e = eArg[i];

                if (e != null)
                {
                    e.resolveTables(f);
                }
            }
        }

        /**
         * Resolves the type of this expression and performs certain
         * transformations and optimisations of the expression tree.
         */
        public void resolveType(Session session)
        {

            Expression e;

            for (int i = iSqlArgStart; i < iArgCount; i++)
            {
                e = eArg[i];

                if (e != null)
                {
                    if (e.isParam())
                    {
                        e.setDataType(iArgType[i]);

                        e.nullability = getArgNullability(i);
                        e.valueClassName = getArgClass(i).Name;
                    }
                    else
                    {
                        e.resolveTypes(session);
                    }
                }
            }
        }

        /**
         * Checks each of this object's arguments for resolution, throwing an
         * HsqlException if any arguments have not yet been resolved. <p>
         * The check bool argument is passed on to further check calls.<p>
         */
        public bool checkResolved(bool check)
        {

            bool result = true;

            for (int i = iSqlArgStart; i < iArgCount; i++)
            {
                if (eArg[i] != null)
                {
                    result = result && eArg[i].checkResolved(check);
                }
            }

            return result;
        }

        /**
         * Returns the type of the argument at the specified
         * offset _in this Function object's paramter list. <p>
         */
        public int getArgType(int i)
        {
            return iArgType[i];
        }

        /**
         * Returns the type of this Function
         * object's return type. <p>
         */
        public int getReturnType()
        {
            return iReturnType;
        }

        /**
         * Binds the specified expression to the specified position _in this
         * Function object's parameter list. <p>
         */
        public void setArgument(int i, Expression e)
        {

            if (bConnection)
            {
                i++;
            }

            eArg[i] = e;
            hasAggregate = hasAggregate || (e != null && e.isAggregate());
        }

        /**
         * Returns a DDL representation of this object. <p>
         */
        public string getDLL()
        {

            StringBuilder sb = new StringBuilder();

            // get the name as used by the CHECK statement
            string ddlName = name;

            if (isSimple)
            {
                return name;
            }
            else if (Token.T_TRIM.Equals(name))
            {

                // special case for TRIM
                sb.Append(name).Append('(');

                bool leading = eArg[2].testCondition(null);
                bool trailing = eArg[3].testCondition(null);

                if (leading && trailing)
                {
                    sb.Append(Token.T_BOTH);
                }
                else
                {
                    sb.Append(leading ? Token.T_LEADING
                                      : Token.T_TRAILING);
                }

                // to do change to string
                sb.Append(' ');

                string charval = (String)eArg[1].getValue(null);

                sb.Append(Column.createSQLString(charval)).Append(' ');
                sb.Append(Token.T_FROM).Append(' ');
                sb.Append(eArg[0].getDDL()).Append(')');

                return sb.ToString();
            }

            if (sFunction.Equals(name))
            {
                ddlName = StringConverter.toQuotedString(name, '"', true);
            }

            sb.Append(ddlName).Append('(');

            for (int i = iSqlArgStart; i < eArg.Length; i++)
            {
                sb.Append(eArg[i].getDDL());

                if (i < eArg.Length - 1)
                {
                    sb.Append(',');
                }
            }

            sb.Append(')');

            return sb.ToString();
        }

        /**
         * Returns a string representation of this object. <p>
         */
        public string describe(Session session)
        {

            StringBuilder sb = new StringBuilder();

            sb.Append(base.ToString()).Append("=[\n");
            sb.Append(sFunction).Append("(");

            for (int i = iSqlArgStart; i < eArg.Length; i++)
            {
                sb.Append("[").Append(eArg[i].describe(session)).Append("]");
            }

            sb.Append(") returns ").Append(Types.getTypeString(getReturnType()));
            sb.Append("]\n");

            return sb.ToString();
        }

        /**
         * Returns the Java Class of the object returned by getValue(). <p>
         */
        public string getReturnClassName()
        {
            return returnClassName;
        }

        /**
         * Returns the Java Class of the i'th argument. <p>
         */
        public Type getArgClass(int i)
        {
            return aArgClasses[i];
        }

        /**
         * Returns the SQL nullability code of the i'th argument. <p>
         */
        int getArgNullability(int i)
        {
            return bArgNullable[i] ? Expression.NULLABLE
                                   : Expression.NO_NULLS;
        }

        public MethodInfo getMethod()
        {
            return mMethod;
        }
    }
}